package cn.akwangl.aop;

import cn.akwangl.interceptor.LoggerWrapper;
import cn.hutool.extra.servlet.ServletUtil;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.util.ContentCachingRequestWrapper;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


/**
 * 记录http请求日志,会保存请求前参数以及响应参数.
 *
 * <p>
 * 注意: 在启用异步时,获取{@code  HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();}
 * 会存在线程共享数据问题,因为{@link RequestContextHolder#getRequestAttributes()}的实现用的是{@link ThreadLocal}(ThreadLocal是起到线程隔离,每个线程只能访问自己的ThreadLocal),
 * 所以这是一个现象级问题,下面链接详细讲解了为什么会发生这样问题的原因.<br/>
 * 那要怎么解决这样的问题?<br/>
 * 1. 如果你跟我一样只是获取{@link HttpServletRequest}或{@link HttpServletResponse}对象记录HTTP请求参数的话,那么在获取时只需要判断非空就可以了,在http请求时这两个对象绝对不为空,但如果在异步线程中访问一定会有空指针问题,因为底层实现用的是{@link ThreadLocal}.<br/>
 * 2. 如果你的子线程需要用到{@link HttpServletRequest}或{@link HttpServletResponse}这两个对象,只需要将它还在父线程中提前获取(备份)在需要使用方法中,手动的传参就可以解决此问题.<br/>
 * </p>
 *
 * <a href="https://www.jianshu.com/p/3a3dc1c9bf4c">使用RequestContextHolder.getRequestAttributes()的注意事项</a>
 *
 * @author youyongkun
 * @date 2022/5/17 9:08
 * @since 0.0.4
 */
@Aspect
@Component
public class HttpLogAspect {

    private static final Logger log = LoggerWrapper.getLogger(HttpLogAspect.class);

    /**
     * Controller层切点
     * <p>
     * expression="execution(* com.controller.*.*(..))" 切入点为com.controller包下的类的任意方法
     * expression="execution(* com.controller..*.*(..))" 切入点为com.controller包下子包的类的任意方法
     *
     * @author youyongkun
     * @date 2022/5/17 9:09
     * @since 0.0.2
     */
    @Pointcut("execution (public * cn.akwangl.controller..*.*(..))")
    public void controllerAspect() {
    }

    /**
     * 记录Controller切点 Http 请求参数与响应.
     *
     * @param point 切点
     * @return 切点返回响应数据
     * @author youyongkun
     * @date 2022/5/17 9:25
     * @since 0.0.2
     */
    @Around("controllerAspect()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 开始时间
        long startTimestamp = System.currentTimeMillis();
        ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
        if (servletRequestAttributes != null) {
            HttpServletRequest request = servletRequestAttributes.getRequest();
            log.info(getPreInfo(request));
        }
        // 执行方法
        Object result = point.proceed();
        // 结束时间
        long endTimestamp = System.currentTimeMillis();
        // 运行时长
        long runTime = endTimestamp - startTimestamp;
        if (servletRequestAttributes != null) {
            HttpServletResponse response = servletRequestAttributes.getResponse();
            log.info(getAfterInfo(response, result, runTime));
        }
        return result;
    }

    /**
     * 获取请求之前日志.
     *
     * @param request servlet请求实体
     * @return 请求前日志
     * @author youyongkun
     * @date 2022/5/16 20:59
     */
    private String getPreInfo(HttpServletRequest request) {
        //请求路径
        String url = request.getRequestURI();
        //获取请求参数信息
        String paramData = null;
        //设置请求方法
        String method = request.getMethod();

        if (RequestMethod.GET.name().equals(method)) {
            paramData = JSONObject.toJSONString(request.getParameterMap(),
                    SerializerFeature.DisableCircularReferenceDetect,
                    SerializerFeature.WriteMapNullValue);
        } else if (RequestMethod.POST.name().equals(method)) {
            if (request instanceof ContentCachingRequestWrapper) {
                ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request;
                paramData = new String(requestWrapper.getContentAsByteArray());
            } else {
                paramData = "接口为上传类型，不打印参数";
            }
        }
        String clientIp = ServletUtil.getClientIP(request);
        String preInfo = String.format("请求地址:%s,请求方法:%s,方法参数:%s,客户端ip:%s", url, method, paramData, clientIp);
        return preInfo;
    }

    /**
     * 获取请求之后日志
     *
     * @param response servlet响应实体
     * @param result   响应数据
     * @param runTime  运行时长 毫秒
     * @return 请求响应日志
     * @author youyongkun
     * @date 2022/5/16 21:07
     */
    private String getAfterInfo(HttpServletResponse response, Object result, long runTime) {
        //获取请求错误码
        int status = response.getStatus();
        String responseBody = null;
        if (result != null) {
            try {
                responseBody = JSONObject.toJSONString(result);
            } catch (JSONException e) {
                responseBody = result.toString();
            }
        }
        String afterInfo = String.format("响应状态码:%s,响应json:%s,耗时:%s毫秒", status, responseBody, runTime);
        return afterInfo;
    }
}
